// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// protocol_gen (re)generates the cppdap .h and .cpp files that describe the
// DAP protocol.
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	"reflect"
	"runtime"
	"sort"
	"strings"
)

const (
	protocolURL = "https://raw.githubusercontent.com/microsoft/vscode-debugadapter-node/master/debugProtocol.json"
	packageURL  = "https://raw.githubusercontent.com/microsoft/vscode-debugadapter-node/master/protocol/package.json"

	versionTag     = "${version}"
	commonPrologue = `// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Generated with protocol_gen.go -- do not edit this file.
//   go run scripts/protocol_gen/protocol_gen.go
//
// DAP version ${version}
`

	headerPrologue = commonPrologue + `
#ifndef dap_protocol_h
#define dap_protocol_h

#include "optional.h"
#include "typeinfo.h"
#include "typeof.h"
#include "variant.h"

#include <string>
#include <type_traits>
#include <vector>

namespace dap {

struct Request {};
struct Response {};
struct Event {};

`

	headerEpilogue = `}  // namespace dap

#endif  // dap_protocol_h
`

	cppPrologue = commonPrologue + `

#include "dap/protocol.h"

namespace dap {

`

	cppEpilogue = `}  // namespace dap
`
)

func main() {
	flag.Parse()
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

// root object of the parsed schema
type root struct {
	Schema      string                 `json:"$schema"`
	Title       string                 `json:"title"`
	Description string                 `json:"description"`
	Ty          string                 `json:"type"`
	Definitions map[string]*definition `json:"definitions"`
}

// definitions() returns a lexicographically-stored list of named definitions
func (r *root) definitions() []namedDefinition {
	sortedDefinitions := make([]namedDefinition, 0, len(r.Definitions))
	for name, def := range r.Definitions {
		sortedDefinitions = append(sortedDefinitions, namedDefinition{name, def})
	}
	sort.Slice(sortedDefinitions, func(i, j int) bool { return sortedDefinitions[i].name < sortedDefinitions[j].name })
	return sortedDefinitions
}

// getRef() returns the namedDefinition with the given reference string
// References have the form '#/definitions/<name>'
func (r *root) getRef(ref string) (namedDefinition, error) {
	if !strings.HasPrefix(ref, "#/definitions/") {
		return namedDefinition{}, fmt.Errorf("Unknown $ref '%s'", ref)
	}
	name := strings.TrimPrefix(ref, "#/definitions/")
	def, ok := r.Definitions[name]
	if !ok {
		return namedDefinition{}, fmt.Errorf("Unknown $ref '%s'", ref)
	}
	return namedDefinition{name, def}, nil
}

// namedDefinition is a [name, definition] pair
type namedDefinition struct {
	name string      // name as defined in the schema
	def  *definition // definition node
}

// definition is the core JSON object type in the schema, describing requests,
// responses, events, properties and more.
type definition struct {
	Ty          interface{}   `json:"type"`
	Title       string        `json:"title"`
	Items       *definition   `json:"items"`
	Description string        `json:"description"`
	Properties  properties    `json:"properties"`
	Required    []string      `json:"required"`
	AllOf       []*definition `json:"allOf"`
	Ref         string        `json:"$ref"`
	OpenEnum    []string      `json:"_enum"`
	ClosedEnum  []string      `json:"enum"`

	// The resolved C++ type of the definition
	cppType cppType
}

// properties is a map of property name to the property definition
type properties map[string]*definition

// foreach() calls cb for each property in the map. cb is called in
// lexicographically-stored order for deterministic processing.
func (p *properties) foreach(cb func(string, *definition) error) error {
	sorted := make([]namedDefinition, 0, len(*p))
	for name, property := range *p {
		sorted = append(sorted, namedDefinition{name, property})
	}
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].name < sorted[j].name })
	for _, entry := range sorted {
		if err := cb(entry.name, entry.def); err != nil {
			return err
		}
	}
	return nil
}

// cppField describes a single C++ field of a C++ structure
type cppField struct {
	desc     string
	ty       cppType
	name     string
	optional bool
}

// cppType is an interface for all C++ generated types
type cppType interface {
	// Name() returns the type name, used to refer to the type
	Name() string
	// Dependencies() returns a list of dependent types, which must be emitted
	// before this type
	Dependencies() []cppType
	// File() returns the cppTargetFile that this type should be written to
	File() cppTargetFile
	// Description() returns the type description as parsed from the schema
	Description() string
	// DefaultValue() returns the default value that should be used for any
	// fields of this type
	DefaultValue() string
	// WriteHeader() writes the type definition to the given .h file writer
	WriteHeader(w io.Writer)
	// WriteHeader() writes the type definition to the given .cpp file writer
	WriteCPP(w io.Writer)
}

// cppStruct implements the cppType interface, describing a C++ structure
type cppStruct struct {
	name      string        // C++ type name
	protoname string        // DAP name
	desc      string        // Description
	base      string        // Base class name
	fields    []cppField    // All fields of the structure
	deps      []cppType     // Types this structure depends on
	typedefs  []cppTypedef  // All nested typedefs
	file      cppTargetFile // The files this type should be written to
}

func (s *cppStruct) Name() string            { return s.name }
func (s *cppStruct) Dependencies() []cppType { return s.deps }
func (s *cppStruct) File() cppTargetFile     { return s.file }
func (s *cppStruct) Description() string     { return s.desc }
func (s *cppStruct) DefaultValue() string    { return "" }
func (s *cppStruct) WriteHeader(w io.Writer) {
	if s.desc != "" {
		io.WriteString(w, "// ")
		io.WriteString(w, strings.ReplaceAll(s.desc, "\n", "\n// "))
		io.WriteString(w, "\n")
	}
	io.WriteString(w, "struct ")
	io.WriteString(w, s.name)
	if s.base != "" {
		io.WriteString(w, " : public ")
		io.WriteString(w, s.base)
	}
	io.WriteString(w, " {")

	// typedefs
	for _, t := range s.typedefs {
		io.WriteString(w, "\n  using ")
		io.WriteString(w, t.from)
		io.WriteString(w, " = ")
		io.WriteString(w, t.to.Name())
		io.WriteString(w, ";")
	}

	for _, f := range s.fields {
		if f.desc != "" {
			io.WriteString(w, "\n  // ")
			io.WriteString(w, strings.ReplaceAll(f.desc, "\n", "\n  // "))
		}
		io.WriteString(w, "\n  ")
		if f.optional {
			io.WriteString(w, "optional<")
			io.WriteString(w, f.ty.Name())
			io.WriteString(w, ">")
		} else {
			io.WriteString(w, f.ty.Name())
		}
		io.WriteString(w, " ")
		io.WriteString(w, sanitize(f.name))
		if !f.optional && f.ty.DefaultValue() != "" {
			io.WriteString(w, " = ")
			io.WriteString(w, f.ty.DefaultValue())
		}
		io.WriteString(w, ";")
	}

	io.WriteString(w, "\n};\n\n")

	io.WriteString(w, "DAP_DECLARE_STRUCT_TYPEINFO(")
	io.WriteString(w, s.name)
	io.WriteString(w, ");\n\n")
}
func (s *cppStruct) WriteCPP(w io.Writer) {
	// typeinfo
	io.WriteString(w, "DAP_IMPLEMENT_STRUCT_TYPEINFO(")
	io.WriteString(w, s.name)
	io.WriteString(w, ",\n                    \"")
	io.WriteString(w, s.protoname)
	io.WriteString(w, "\"")
	for _, f := range s.fields {
		io.WriteString(w, ",\n                    ")
		io.WriteString(w, "DAP_FIELD(")
		io.WriteString(w, sanitize(f.name))
		io.WriteString(w, ", \"")
		io.WriteString(w, f.name)
		io.WriteString(w, "\")")
	}
	io.WriteString(w, ");\n\n")
}

// cppStruct implements the cppType interface, describing a C++ typedef
type cppTypedef struct {
	from string  // Name of the typedef
	to   cppType // Target of the typedef
	desc string  // Description
}

func (ty *cppTypedef) Name() string            { return ty.from }
func (ty *cppTypedef) Dependencies() []cppType { return []cppType{ty.to} }
func (ty *cppTypedef) File() cppTargetFile     { return types }
func (ty *cppTypedef) Description() string     { return ty.desc }
func (ty *cppTypedef) DefaultValue() string    { return ty.to.DefaultValue() }
func (ty *cppTypedef) WriteHeader(w io.Writer) {
	if ty.desc != "" {
		io.WriteString(w, "// ")
		io.WriteString(w, strings.ReplaceAll(ty.desc, "\n", "\n// "))
		io.WriteString(w, "\n")
	}

	io.WriteString(w, "using ")
	io.WriteString(w, ty.from)
	io.WriteString(w, " = ")
	io.WriteString(w, ty.to.Name())
	io.WriteString(w, ";\n\n")
}
func (ty *cppTypedef) WriteCPP(w io.Writer) {}

// cppStruct implements the cppType interface, describing a basic C++ type
type cppBasicType struct {
	name         string    // Type name
	desc         string    // Description
	deps         []cppType // Types this type depends on
	defaultValue string    // Default value for fields of this type
}

func (ty *cppBasicType) Name() string            { return ty.name }
func (ty *cppBasicType) Dependencies() []cppType { return ty.deps }
func (ty *cppBasicType) File() cppTargetFile     { return types }
func (ty *cppBasicType) Description() string     { return ty.desc }
func (ty *cppBasicType) DefaultValue() string    { return ty.defaultValue }
func (ty *cppBasicType) WriteHeader(w io.Writer) {}
func (ty *cppBasicType) WriteCPP(w io.Writer)    {}

// sanitize() returns the given identifier transformed into a legal C++ identifier
func sanitize(s string) string {
	s = strings.Trim(s, "_")
	switch s {
	case "default":
		return "def"
	default:
		return s
	}
}

// appendEnumDetails() appends any enumerator details to the given description string.
func appendEnumDetails(desc string, openEnum []string, closedEnum []string) string {
	if len(closedEnum) > 0 {
		desc += "\n\nMust be one of the following enumeration values:\n"
		for i, enum := range closedEnum {
			if i > 0 {
				desc += ", "
			}
			desc += "'" + enum + "'"
		}
	}

	if len(openEnum) > 0 {
		desc += "\n\nMay be one of the following enumeration values:\n"
		for i, enum := range openEnum {
			if i > 0 {
				desc += ", "
			}
			desc += "'" + enum + "'"
		}
	}
	return desc
}

// buildRootStruct() populates the cppStruct type with information found in def.
// buildRootStruct() must only be called after all the root definitions have had
// a type constructed (however, not necessarily fully populated)
func (r *root) buildRootStruct(ty *cppStruct, def *definition) error {
	if len(def.AllOf) > 1 && def.AllOf[0].Ref != "" {
		ref, err := r.getRef(def.AllOf[0].Ref)
		if err != nil {
			return err
		}
		ty.base = ref.name
		if len(def.AllOf) > 2 {
			return fmt.Errorf("Cannot handle allOf with more than 2 entries")
		}
		def = def.AllOf[1]
	}

	if def.Ty != "object" {
		return fmt.Errorf("Definion '%v' was of unexpected type '%v'", ty.name, def.Ty)
	}

	ty.desc = def.Description

	var body *definition
	var err error
	switch ty.base {
	case "Request":
		if arguments, ok := def.Properties["arguments"]; ok {
			body = arguments
		}
		if command, ok := def.Properties["command"]; ok {
			ty.protoname = command.ClosedEnum[0]
		}
		responseName := strings.TrimSuffix(ty.name, "Request") + "Response"
		responseDef := r.Definitions[responseName]
		responseTy := responseDef.cppType
		if responseTy == nil {
			return fmt.Errorf("Failed to find response type '%v'", responseName)
		}
		ty.deps = append(ty.deps, responseTy)
		ty.typedefs = append(ty.typedefs, cppTypedef{from: "Response", to: responseTy})
		ty.file = request
	case "Response":
		body = def.Properties["body"]
		ty.file = response
	case "Event":
		body = def.Properties["body"]
		if command, ok := def.Properties["event"]; ok {
			ty.protoname = command.ClosedEnum[0]
		}
		ty.file = event
	default:
		body = def
		ty.file = types
	}
	if err != nil {
		return err
	}

	if body == nil {
		return nil
	}
	if body.Ref != "" {
		ref, err := r.getRef(body.Ref)
		if err != nil {
			return err
		}
		body = ref.def
	}

	required := make(map[string]bool, len(body.Required))
	for _, r := range body.Required {
		required[r] = true
	}

	if err = body.Properties.foreach(func(propName string, property *definition) error {
		propTy, err := r.getType(property)
		if err != nil {
			return fmt.Errorf("While processing %v.%v: %v", ty.name, propName, err)
		}

		optional := !required[propName]
		desc := appendEnumDetails(property.Description, property.OpenEnum, property.ClosedEnum)
		ty.fields = append(ty.fields, cppField{
			desc:     desc,
			ty:       propTy,
			name:     propName,
			optional: optional,
		})

		ty.deps = append(ty.deps, propTy)

		return nil
	}); err != nil {
		return err
	}

	return nil
}

// getType() returns the cppType for the given definition
func (r *root) getType(def *definition) (builtType cppType, err error) {
	if def.cppType != nil {
		return def.cppType, nil
	}
	defer func() { def.cppType = builtType }()

	if def.Ref != "" {
		ref, err := r.getRef(def.Ref)
		if err != nil {
			return nil, err
		}
		return ref.def.cppType, nil
	}

	v := reflect.ValueOf(def.Ty)

	if v.Kind() == reflect.Interface {
		v = v.Elem()
	}

	var typeof func(reflect.Value) (cppType, error)
	typeof = func(v reflect.Value) (cppType, error) {
		if v.Kind() == reflect.Interface {
			v = v.Elem()
		}
		switch v.Kind() {
		case reflect.String:
			ty := v.Interface().(string)
			switch ty {
			case "string":
				desc := appendEnumDetails(def.Description, nil, def.ClosedEnum)
				defaultValue := ""
				if len(def.ClosedEnum) > 0 {
					defaultValue = `"` + def.ClosedEnum[0] + `"`
				}
				ty := &cppBasicType{
					name:         ty,
					defaultValue: defaultValue,
					desc:         desc,
				}
				return ty, nil

			case "object", "boolean", "integer", "number", "null":
				ty := &cppBasicType{
					name: ty,
					desc: def.Description,
				}
				return ty, nil
			case "array":
				name := "array<any>"
				deps := []cppType{}
				if def.Items != nil {
					elTy, err := r.getType(def.Items)
					if err != nil {
						return nil, err
					}
					name = fmt.Sprintf("array<%s>", elTy.Name())
					deps = append(deps, elTy)
				}
				return &cppBasicType{
					name: name,
					desc: def.Description,
					deps: deps,
				}, nil
			default:
				return nil, fmt.Errorf("Unhandled property type '%v'", ty)
			}
		case reflect.Slice, reflect.Array:
			args := []string{}
			deps := []cppType{}
			for i := 0; i < v.Len(); i++ {
				elTy, err := typeof(v.Index(i))
				if err != nil {
					return nil, err
				}
				deps = append(deps, elTy)
				args = append(args, elTy.Name())
			}
			return &cppBasicType{
				name: "variant<" + strings.Join(args, ", ") + ">",
				desc: def.Description,
				deps: deps,
			}, nil
		}
		return nil, fmt.Errorf("Unsupported type '%v' kind: %v", v.Interface(), v.Kind())
	}

	return typeof(v)
}

// buildTypes() builds all the reachable types found in the schema, returning
// all the root, named definition types.
func (r *root) buildTypes() ([]cppType, error) {
	ignore := map[string]bool{
		// These are handled internally.
		"ProtocolMessage": true,
		"Request":         true,
		"Event":           true,
		"Response":        true,
	}

	// Step 1: Categorize all the named definitions by type.
	structDefs := []namedDefinition{}
	enumDefs := []namedDefinition{}
	for _, entry := range r.definitions() {
		if ignore[entry.name] {
			continue
		}
		switch entry.def.Ty {
		case nil, "object":
			structDefs = append(structDefs, entry)
		case "string":
			enumDefs = append(enumDefs, entry)
		default:
			return nil, fmt.Errorf("Unhandled top-level definition type: %v", entry.def.Ty)
		}
	}

	// Step 2: Construct, but do not build all the named object types (yet).
	// This allows the getType() function to resolve to the cppStruct types,
	// even if they're not built yet.
	out := []cppType{}
	for _, entry := range structDefs {
		entry.def.cppType = &cppStruct{
			name: entry.name,
		}
		out = append(out, entry.def.cppType)
	}

	// Step 3: Resolve all the enum types
	for _, entry := range enumDefs {
		enumTy, err := r.getType(entry.def)
		if err != nil {
			return nil, err
		}
		ty := &cppTypedef{
			from: entry.name,
			to:   enumTy,
			desc: enumTy.Description(),
		}
		entry.def.cppType = ty
		out = append(out, entry.def.cppType)
	}

	// Step 4: Resolve all the structure types
	for _, s := range structDefs {
		if err := r.buildRootStruct(s.def.cppType.(*cppStruct), s.def); err != nil {
			return nil, err
		}
	}

	return out, nil
}

// cppTargetFile is an enumerator of target files that types should be written
// to.
type cppTargetFile string

const (
	request  = cppTargetFile("request")  // protocol_request.cpp
	response = cppTargetFile("response") // protocol_response.cpp
	event    = cppTargetFile("event")    // protocol_events.cpp
	types    = cppTargetFile("types")    // protocol_types.cpp
)

// cppTargetFilePaths is a map of cppTargetFile to the target file path
type cppTargetFilePaths map[cppTargetFile]string

// cppFiles is a map of cppTargetFile to the open file
type cppFiles map[cppTargetFile]*os.File

// run() loads and parses the package and protocol JSON files, generates the
// protocol types from the schema, writes the types to the C++ files, then runs
// clang-format on each.
func run() error {
	pkg := struct {
		Version string `json:"version"`
	}{}
	if err := loadJSONFile(packageURL, &pkg); err != nil {
		return fmt.Errorf("Failed to load JSON file from '%v': %w", packageURL, err)
	}

	protocol := root{}
	if err := loadJSONFile(protocolURL, &protocol); err != nil {
		return fmt.Errorf("Failed to load JSON file from '%v': %w", protocolURL, err)
	}

	hPath, cppPaths := outputPaths()
	if err := emitFiles(&protocol, hPath, cppPaths, pkg.Version); err != nil {
		return fmt.Errorf("Failed to emit files: %w", err)
	}

	if clangfmt, err := exec.LookPath("clang-format"); err == nil {
		if out, err := exec.Command(clangfmt, "-i", hPath).CombinedOutput(); err != nil {
			return fmt.Errorf("Failed to run clang-format on '%v':\n%v\n%w", hPath, string(out), err)
		}
		for _, p := range cppPaths {
			if out, err := exec.Command(clangfmt, "-i", p).CombinedOutput(); err != nil {
				return fmt.Errorf("Failed to run clang-format on '%v':\n%v\n%w", p, string(out), err)
			}
		}
	} else {
		fmt.Printf("clang-format not found on PATH. Please format before committing.")
	}

	return nil
}

// emitFiles() opens each of the C++ files, generates the cppType definitions
// from the schema root, then writes the types to the C++ files in dependency
// order.
func emitFiles(r *root, hPath string, cppPaths map[cppTargetFile]string, version string) error {
	h, err := os.Create(hPath)
	if err != nil {
		return err
	}
	defer h.Close()
	cppFiles := map[cppTargetFile]*os.File{}
	for ty, p := range cppPaths {
		f, err := os.Create(p)
		if err != nil {
			return err
		}
		cppFiles[ty] = f
		defer f.Close()
	}

	h.WriteString(strings.ReplaceAll(headerPrologue, versionTag, version))
	for _, f := range cppFiles {
		f.WriteString(strings.ReplaceAll(cppPrologue, versionTag, version))
	}

	types, err := r.buildTypes()
	if err != nil {
		return err
	}

	typesByName := map[string]cppType{}
	for _, s := range types {
		typesByName[s.Name()] = s
	}

	seen := map[string]bool{}
	var emit func(cppType) error
	emit = func(ty cppType) error {
		name := ty.Name()
		if seen[name] {
			return nil
		}
		seen[name] = true
		for _, dep := range ty.Dependencies() {
			if err := emit(dep); err != nil {
				return err
			}
		}
		ty.WriteHeader(h)
		ty.WriteCPP(cppFiles[ty.File()])
		return nil
	}

	// emit message types.
	// Referenced types will be transitively emitted.
	for _, s := range types {
		switch s.File() {
		case request, response, event:
			if err := emit(s); err != nil {
				return err
			}
		}
	}

	h.WriteString(headerEpilogue)
	for _, f := range cppFiles {
		f.WriteString(cppEpilogue)
	}

	return nil
}

// loadJSONFile() loads the JSON file from the given URL using a HTTP GET
// request.
func loadJSONFile(url string, obj interface{}) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if err := json.NewDecoder(bytes.NewReader(data)).Decode(obj); err != nil {
		return err
	}
	return nil
}

// outputPaths() returns a path to the target C++ .h file and .cpp files
func outputPaths() (string, cppTargetFilePaths) {
	_, thisFile, _, _ := runtime.Caller(1)
	thisDir := path.Dir(thisFile)
	h := path.Join(thisDir, "../../include/dap/protocol.h")
	cpp := cppTargetFilePaths{
		request:  path.Join(thisDir, "../../src/protocol_requests.cpp"),
		response: path.Join(thisDir, "../../src/protocol_response.cpp"),
		event:    path.Join(thisDir, "../../src/protocol_events.cpp"),
		types:    path.Join(thisDir, "../../src/protocol_types.cpp"),
	}
	return h, cpp
}
